#include <QDebug>
#include <QFile>
#include <QDir>
#include <QFileInfo>
#include <QDirIterator>
#include "ModulesMgr.h"
#include "Scopes/ModuleScope.h"
#include "CramBuilder.h"
#include "RestoreVisitor.h"
#include "BinaryModule.h"

#include "grammar/python3/PythonGrammar30.h"
#include "grammar/python3/PythonGrammar30TokenManager.h"

ModulesMgr::ModulesMgr():
    QObject()
{
}

ModulesMgr::~ModulesMgr()
{
}

ModuleScope* ModulesMgr::moduleByFileName(const QString& fileName, bool useCache)
{
    if (_parsed.contains(fileName))
        return _parsed[fileName];

    qDebug() << fileName;
    if (!QFile::exists(fileName)){
        qDebug() << "not exists";
        return NULL;
    }

    QFileInfo info(fileName);
    ModuleScope *mod = NULL;
    switch(fileType(info)){
    case PyFile:
        mod = parsePyFile(info, info.baseName(), false, useCache);
        break;
    case PyModule:
        QFileInfo modInfo(info.absoluteFilePath()+QDir::separator()+"__init__.py");
        mod = parsePyFile(modInfo, info.baseName(), false, useCache);
        break;
    }
    _parsed[fileName] = mod;

    if (mod)
        mod->buildScope();
    return mod;
}

ModuleScope* ModulesMgr::moduleByContent(const QString& content, const QString& fileName)
{
    CharStream stream(content);
    PythonGrammar30TokenManager in(&stream);
    PythonGrammar30 grammar(&in);

    ModuleScope* module = new ModuleScope(QFileInfo(fileName));
    connect(&grammar, SIGNAL(parseError(QString, int, int)), module, SLOT(onParseError(QString, int, int)));

    ModulePtr newRoot = grammar.file_input().dynamicCast<Module>();
    module->setAst(newRoot);

    if (module)
        module->buildScope();
    return module;
}


ModuleScope* ModulesMgr::moduleByName(const QString& name, const QString& relPath)
{
    if (_parsed.contains(name))
        return _parsed[name];

    qDebug() << "module by name" << name;
    ModuleScope* scope = NULL;
    if (_cache.contains(name)){
        if (_cache.isCachedName(name)){
            scope = _parsePyFile(_cache.getCachedName(name));
        } else {
            scope = _parsePyFile(_cache.fileNameForName(name));
            if (scope)
                saveCache(scope, name);
        }
    } else {
        QString fileName = findModuleByName(name, relPath);
        qDebug() << name << "found in" << fileName;
        if (!fileName.isEmpty() && (fileName.endsWith(".py") || fileName.endsWith(".pyw"))){
            scope = parsePyFile(QFileInfo(fileName), name);
        } else {
            scope = parsePyFile(QFileInfo(binary()->import(name)), name, true);
        }
    }
    _parsed[name] = scope;
    if (scope)
        scope->buildScope();
    return scope;
}

void ModulesMgr::setProjectPath(const QString& path)
{
    _projectPath = path;
}

void ModulesMgr::setImportPathes(const QStringList& pathes)
{
    _importPathes = pathes;
}

// --------------------------------------------------------------

ItemType ModulesMgr::fileType(const QFileInfo& info)
{
    if (info.isDir())
        return PyModule;
    return PyFile;
}

ModuleScope* ModulesMgr::parsePyFile(const QFileInfo& info, const QString& name, bool fromBinary, bool useCache)
{
    ModuleScope* scope = NULL;
    if (useCache && _cache.isCachedFile(info.absoluteFilePath())){
         scope = _parsePyFile(_cache.getCachedFile(info.absoluteFilePath()));
    } else {
        scope = _parsePyFile(info.absoluteFilePath());
        _cache.addToCache(info.absoluteFilePath(), name, fromBinary);
        saveCache(scope, name);
    }
    return scope;
}

ModuleScope* ModulesMgr::_parsePyFile(const QString& fileName)
{
    qDebug() << "parse file" << fileName;
    QFile file(fileName);
    if (!file.open(QIODevice::ReadOnly))
        return NULL;

    QString test = file.readAll();
    file.close();

    CharStream stream(test);
    PythonGrammar30TokenManager in(&stream);
    PythonGrammar30 grammar(&in);

    ModuleScope* module = new ModuleScope(fileName);
    connect(&grammar, SIGNAL(parseError(QString, int, int)), module, SLOT(onParseError(QString, int, int)));

    ModulePtr newRoot = grammar.file_input().dynamicCast<Module>();
    module->setAst(newRoot);
    return module;
}


void ModulesMgr::saveCache(ModuleScope* scope, const QString& name)
{
    CramModule boot(name);
    RestoreVisitor visit(&boot);
    visit.traverse(scope->ast());

    QFile out(_cache.getCachedName(name));
    if (out.open(QIODevice::WriteOnly)){
        out.write(boot.build().toUtf8());
    }
    _cache.updateMd5(name);
}

QString ModulesMgr::findModuleByName(const QString& importName, const QString& relPath)
{
    QFileInfo ret;
    if (!_importPathes.length())
        setImportPathes();

    if (importName.startsWith(".")){
        // relative import
    } else {
        if (!_projectPath.isEmpty()){
            ret = findModuleInPathes(importName, QStringList() << _projectPath);
            if (!ret.absoluteFilePath().isEmpty())
                return ret.absoluteFilePath();
        }
        ret = findModuleInPathes(importName, _importPathes);
    }
    return ret.absoluteFilePath();
}

QFileInfo ModulesMgr::findModuleInPathes(const QString importName, const QStringList& pathes)
{
    QFileInfo ret;
    QStringList chunks = importName.split(".");
    QString fname = chunks.takeFirst();

    ret = findInPathes(fname, _importPathes);

    if (ret.isDir()){
        foreach(QString fchnuhk, chunks) {
            QFileInfo path = findInPathes(fchnuhk, QStringList() << ret.absoluteFilePath());
            if (path.exists())
                ret = path;
        }
    }
    if (ret.isDir()){
        ret = QFileInfo(ret.absoluteFilePath()+QDir::separator()+"__init__.py");
    }
    return ret;
}


QFileInfo ModulesMgr::findInPathes(const QString name, const QStringList& pathes)
{
    foreach(QString path, pathes){
        QDirIterator iter(path, QDirIterator::NoIteratorFlags);
        while(iter.hasNext()){
            iter.next();
            QFileInfo info = iter.fileInfo();
            if (info.baseName() == name)
                return info;
        }
    }
    return QFileInfo();
}

void ModulesMgr::setImportPathes()
{
    _importPathes = binary()->importPathes();
    qDebug() << _importPathes;
}

BinaryModule* ModulesMgr::binary()
{
    if (!_binary)
        _binary = new BinaryModule();
    return _binary;
}
